import hashlib
import json
from lxml import etree
from bs4 import BeautifulSoup
import re
import os


# HTML元素工具类
class HtmlUtils:
    @staticmethod
    def extract_text_from_tag_node(tag_node):
        """
        提取指定HTML标签下的文本数据
        :param tag_node: html标签节点
        :return:
        """
        if type(tag_node) != etree._Element:
            raise Exception(tag_node, " is not a valid element")
        text_content = etree.tostring(tag_node).decode('utf-8').replace('\n', '').replace('\r', '').replace('\r\n', '')
        if text_content:
            text_content = TextUtils.remove_html_tags(text_content)
            text_content = TextUtils.remove_consecutive_spaces(text_content)
            return text_content.strip()
        else:
            return None

    # region 表格相关工具类
    @staticmethod
    def extract_data_from_table_thead(thead_node):
        """
        处理表格thead节点的函数
        :param thead_node:
        :return:
        """
        trs = thead_node.xpath("//thead//tr")
        thead_value = []
        for th in trs[0]:
            thead_value.append(th.text)
        return thead_value

    @staticmethod
    def extract_data_from_table_tbody(tbody_node):
        """
        处理表格tbody节点的函数
        :param tbody_node:
        :return:
        """
        tbody_value = []
        for tr in tbody_node:
            item = []
            for td in tr:
                item.append(HtmlUtils.extract_data_from_table_td(td))
            tbody_value.append(item)
        return tbody_value

    @staticmethod
    def extract_data_from_table_td(element_td):
        """
        获取指定td下所有的数据
        :param element_td:
        :return:
        """
        value_array = [str(element_td.text)]
        child_elements = element_td.xpath('.//*')
        for element_item in child_elements:
            if element_item.text is None or element_item.text == "\xa0":
                continue
            value_array.append(TextUtils.remove_consecutive_spaces(str(element_item.text)))
        return "\n".join(value_array)

    @staticmethod
    def extract_data_from_table(table_node):
        # 获取表格的thead和tbody列表
        thead_node_list = table_node.xpath(".//thead")
        tbody_node_list = table_node.xpath(".//tbody")
        thead_value = []
        tbody_value = []
        for thead_node in thead_node_list:
            thead_value = HtmlUtils._extract_data_from_thead(thead_node)
        for tbody_node in tbody_node_list:
            tbody_value = HtmlUtils._extract_data_from_tbody(tbody_node)
        return {
            "thead": thead_value,
            "tbody": tbody_value
        }

    @staticmethod
    def _extract_data_from_thead(thead_node):
        thead_value_list = []
        tr_node_list = thead_node.xpath("./tr")
        for tr_node in tr_node_list:
            # 查找tr中的td -> 返回值为列表
            tr_value_list = []
            td_node_list_simple = tr_node.xpath("./th")
            for td_node in td_node_list_simple:
                # 获取td中的文本内容 todo 判空
                td_value = HtmlUtils.extract_text_from_tag_node(td_node)
                tr_value_list.append(td_value)
            thead_value_list.append(tr_value_list)
        return thead_value_list

    @staticmethod
    def _extract_data_from_tbody(tbody_node, is_format=False):
        """
        获取tbody中的数据
        :param tbody_node: tbody节点
        :param is_format: 是否格式化，即：是否需要按照表格内中内容自己的格式输出
        """
        tbody_value_list = []
        tr_node_list = tbody_node.xpath("./tr")
        for tr_node in tr_node_list:
            tr_value_list = []
            # 查找tr中的td -> 返回值为列表
            td_node_list_simple = tr_node.xpath("./td")
            for td_node in td_node_list_simple:
                # 获取td中的文本内容
                td_value = HtmlUtils.extract_text_from_tag_node(td_node)
                if is_format:
                    td_value = HtmlUtils._extract_data_from_table_td_to_text(td_node)
                tr_value_list.append(td_value)
            if len(tr_value_list) > 0:
                tbody_value_list.append(tr_value_list)
        return tbody_value_list

    @staticmethod
    def _extract_data_from_table_td_to_text(td_node):
        """
        获取td中的文本内容，并拼接为字符串返回
        """
        td_value_list = HtmlUtils._extract_data_from_table_td_to_list(td_node)
        td_value = ""
        for value in td_value_list:
            if TextUtils.is_string_valid(value):
                td_value = td_value + value + "\n"
        return td_value

    @staticmethod
    def _extract_data_from_table_td_to_list(td_node):
        """
        获取td中的文本内容，返回列表
        """
        children_node_list = td_node.xpath("./*")
        value_list = []
        td_first_value = str(td_node.text)
        if TextUtils.is_string_valid(td_first_value):
            value_list.append(td_first_value)
        for child_node in children_node_list:
            child_value = HtmlUtils.extract_text_from_tag_node(child_node)
            child_value = TextUtils.remove_consecutive_spaces(child_value)
            if TextUtils.is_string_valid(child_value):
                value_list.append(child_value)
            pass
        return value_list

    # endregion

    # region ul-li列表相关工具类
    @staticmethod
    def extract_data_from_ul_li_recursive(ul_node):
        """
        递归处理ul下的li节点
        :param ul_node:
        :return:
        """
        items = []
        for li_element in ul_node.xpath('./li'):
            item = {'value': HtmlUtils.extract_text_from_tag_node(li_element)}
            ul_child = li_element.xpath('./ul')
            if ul_child:
                item['children'] = HtmlUtils.extract_data_from_ul_li_recursive(ul_child[0])
            items.append(item)
        return items

    @staticmethod
    def extract_data_from_ul_li_to_list(ul_node):
        items = []
        for li_element in ul_node.xpath('./li'):
            # todo 此处应该获取text，而非该节点下全部文本???
            item = HtmlUtils.extract_text_from_tag_node(li_element)
            items.append(item)
            ul_child = li_element.xpath('./ul')
            if ul_child:
                items.append(HtmlUtils.extract_data_from_ul_li_to_list(ul_child[0]))
        return items

    @staticmethod
    def parse_ul(ul_element):
        items = []
        for li in ul_element.xpath('./li'):
            item = {'text': HtmlUtils.extract_text_excluding_ul(li)}
            sub_ul = li.xpath('./ul')
            if sub_ul:
                item['subitems'] = HtmlUtils.parse_ul(sub_ul[0])
            items.append(item)
        return items

    @staticmethod
    def extract_text_excluding_ul(li_element):
        """
        递归函数，用于提取li元素及其子元素的文本，但排除ul
        :param li_element:
        :return:
        """
        # 初始化一个空列表来存储文本
        texts = []

        # 递归函数，用于提取元素及其子元素的文本，但排除ul
        def extract_text_recursive(element):
            if element.tag != 'ul':
                # 添加元素自身的文本（如果有的话）
                if element.text and element.text.strip():
                    # TextUtils.remove_consecutive_spaces(element.text.strip())
                    texts.append(element.text.strip())
                    # 递归处理子元素
                for child in element:
                    extract_text_recursive(child)
                    # 添加元素尾部的文本（如果有的话）
                if element.tail and element.tail.strip():
                    texts.append(element.text.strip())

        # 调用递归函数
        extract_text_recursive(li_element)
        # 合并所有文本
        return ' '.join(texts).strip()

    @staticmethod
    # 定义一个函数来递归地处理li元素
    def process_li(li_element):
        # 初始化结果字典
        result = {"text": "", "subitems": []}

        # 提取li元素自身的文本（如果有的话）
        if li_element.text and li_element.text.strip():
            result["text"] += HtmlUtils.extract_text_excluding_ul(li_element + "第一")
        # 处理子元素
        for child in li_element:
            if child.tag == 'ul':
                # 如果是ul元素，递归处理并添加到subitems中
                result["subitems"].extend(HtmlUtils.process_ul(child))
            else:
                # 如果是其他元素，提取其文本并添加到当前li的文本中
                child_text = HtmlUtils.extract_text_from_tag_node(child)
                if TextUtils.is_string_valid(child_text):
                    result["text"] += child_text

        # 提取li元素的尾部文本（如果有的话） todo 是否没必要？
        if li_element.tail and li_element.tail.strip():
            # result["text"] += li_element.tail.strip()
            result["text"] += HtmlUtils.extract_text_excluding_ul(li_element)

        # 去除文本两端的空白字符
        result["text"] = result["text"].strip()

        # 返回结果
        return result

    @staticmethod
    # 定义一个函数来递归地处理ul元素
    def process_ul(ul_element):
        subitems = []
        for li_element in ul_element:
            subitems.append(HtmlUtils.process_li(li_element))
        return subitems

    # endregion
    # region 针对整个页面内容的直接提取
    @staticmethod
    def extract_text_from_html_path(html_path):
        """
        提取html文件中的文本内容
        :param html_path:
        :return:
        """
        if not FileUtils.file_is_exist(html_path):
            raise Exception("file not exist")
        with open(html_path, "rb") as f:
            html_content = f.read()
        tree = etree.HTML(html_content)
        body_nodes = tree.xpath('//body/*')
        text_content = ""
        for text_node in body_nodes:
            text_content = text_content + HtmlUtils.extract_text_from_tag_node(text_node)
        return text_content

    @staticmethod
    def extract_text_list_from_html_path(html_path):
        """
        提取html文件中的文本内容
        :param html_path:
        :return:
        """
        if not FileUtils.file_is_exist(html_path):
            raise Exception("file not exist")
        with open(html_path, "rb") as f:
            html_content = f.read()
        tree = etree.HTML(html_content)
        body_nodes = tree.xpath('//body/*')
        text_list = []
        for text_node in body_nodes:
            text_item = HtmlUtils.extract_text_from_tag_node(text_node)
            if text_item is not None and len(text_item) > 0:
                text_list.append(text_item)
        return text_list

    # endregion

    # todo 合法性判断仍需完善
    @staticmethod
    def concatenate_text(text_list):
        """
        将文本列表拼接成字符串
        :param text_list:
        :return:
        """
        if type(text_list) != list or len(text_list) == 0:
            return ""
        if all(isinstance(item, str) for item in text_list):
            return "".join(text_list)
        else:
            return json.dumps(text_list, ensure_ascii=False)

    @staticmethod
    def extract_text_from_html_deprecated(html_content, exclude_tags=["script", "style"]):
        """
        提取html中所有的文本内容，并去除指定标签
        :param html_content:
        :param exclude_tags:
        :return:
        """
        # 使用 BeautifulSoup 解析 HTML
        soup = BeautifulSoup(html_content, 'html.parser')

        # 删除 <script> 和 <style> 标签及其内容
        for script in soup(exclude_tags):
            script.decompose()

        # 提取 <body> 标签中的文本内容
        body_text = soup.body.get_text(separator=' ')

        return body_text.strip()  # 去除首尾空白字符)


# 文本工具类
class TextUtils:
    @staticmethod
    def remove_html_tags(text):
        """
        去除文本中的html元素
        :param text:
        :return:
        """
        soup = BeautifulSoup(text, 'html.parser')
        return soup.get_text()

    @staticmethod
    def remove_consecutive_spaces(text):
        """
        去除文本中连续的空格与回车
        :param text:
        :return:
        """
        # return re.sub(r'\s+', ' ', text)
        # 去除字符串两端的空白字符
        text = text.strip()
        # 使用正则表达式替换多个空白字符为一个空格
        text = re.sub(r'\s+', ' ', text)
        return text

    @staticmethod
    def contains_word(text, word):
        """
        判断文本中是否包含指定单词
        :param text: 文本
        :param word: 指定的单词
        :return: 如果包含则返回True，否则返回False
        """
        if re.search(r'\b' + re.escape(word) + r'\b', text):
            return True
        else:
            return False

    @staticmethod
    def contains_word_list(text, word_list):
        """
        判断文本中是否包含指定单词列表中的任意一个单词
        :param text: 文本
        :param word_list: 指定的单词列表
        :return: 如果包含则返回True，否则返回False
        """
        for word in word_list:
            if re.search(r'\b' + re.escape(word) + r'\b', text):
                return True
        return False

    @staticmethod
    def remove_brackets_and_contents(text):
        """
        去除文本中的括号及其内容
        :param text:
        :return:
        """
        pattern = r'\[[^\]]*\]|\([^)]*\)|\{[^}]*\}'
        return re.sub(pattern, '', text)

    @staticmethod
    def concatenate_strings_with_separator(string_list, separator=""):
        """
        将字符串列表使用指定分隔符连接起来
        :param string_list:
        :param separator:
        :return:
        """
        # 检查列表中的每个元素，确保它们都是字符串
        if not all(isinstance(s, str) for s in string_list):
            # 错误处理 抛异常
            raise TypeError("在字符串列表拼接为字符串时，列表中的元素必须是字符串类型。")
        return separator.join(string_list)

    @staticmethod
    def is_string_valid(input_string):
        """
        判断字符串是否有效，去除连续空格、换行符、制表符等无效字符后，检查是否有内容。
        :param input_string: 输入的字符串
        :return: 如果字符串有效则返回True，否则返回False
        """
        # 移除前后空格，并且替换掉所有的连续空格、换行符、制表符等无效字符
        stripped_s = ''.join(input_string.split())
        # 检查处理后的字符串是否非空
        return bool(stripped_s)


# 文件工具类
class FileUtils:
    @staticmethod
    def file_is_exist(filepath, is_create=False, is_clear=False):
        """
        判断文件是否存在
        :param filepath: 文件路径
        :param is_create: 如果文件不存在，是否创建，默认不创建
        :param is_clear: 如果文件存在，是否清空，默认不清空
        :return:
        """
        is_exist = os.path.exists(filepath)
        if is_exist:
            if is_clear:
                with open(filepath, 'w', encoding='utf-8') as file:
                    file.write('')
            return True
        else:
            if is_create:
                with open(filepath, 'w', encoding='utf-8') as file:
                    pass
            else:
                return False

    @staticmethod
    def dir_is_exist(dir_path, is_create=False):
        """
        判断文件夹是否存在
        :param dir_path: 文件夹全路径
        :param is_create: 如果文件夹不存在，是否创建，默认不创建
        :return:
        """
        if os.path.exists(dir_path) and os.path.isdir(dir_path):
            return True
        else:
            if is_create:
                os.makedirs(dir_path)
                return True
            else:
                return False

    @staticmethod
    def append_data(filepath, data):
        """
        向文件中追加数据
        :param filepath:
        :param data:
        :return:
        """
        if data:
            FileUtils.file_is_exist(filepath, True)
            with open(filepath, 'a', encoding='utf-8') as file:
                file.write(data)

    @staticmethod
    def append_data_new_line(filepath, data):
        """
        向文件中追加数据，并换行
        :param filepath:
        :param data:
        :return:
        """
        if data:
            FileUtils.append_data(filepath, data + '\n')


class ListUtils:
    @staticmethod
    def flatten_list(nested_list):
        flattened_list = []
        for i in nested_list:
            if isinstance(i, list):
                flattened_list.extend(ListUtils.flatten_list(i))
            else:
                flattened_list.append(i)
        return flattened_list

    @staticmethod
    def remove_invalid_nesting(nested_list):
        """
        递归去除嵌套列表中的无效嵌套
        :param nested_list: 嵌套列表
        :return: 去除无效嵌套后的列表
        """
        while isinstance(nested_list, list) and len(nested_list) == 1:
            nested_list = nested_list[0]

        if isinstance(nested_list, list):
            return [ListUtils.remove_invalid_nesting(item) for item in nested_list]
        else:
            # 如果nested_list不是列表，则将其封装在一个列表中返回
            return [nested_list]

    @staticmethod
    def remove_list_nesting(nested_list):
        """
        递归去除嵌套列表中的所有的列表嵌套，仅留最外层
        :param nested_list:
        :return:
        """
        flattened = []
        for item in nested_list:
            if isinstance(item, list):
                # 如果当前元素是列表，递归调用flatten_nested_list
                flattened.extend(ListUtils.remove_list_nesting(item))
            elif isinstance(item, str):
                # 如果当前元素是字符串，直接添加到flattened列表中
                flattened.append(item)
        return flattened

    @staticmethod
    def show_list(target_list):
        for item in target_list:
            print(item)


# 格式化工具类
class FormatUtils:
    @staticmethod
    def format_object_to_json(data, indent=2):
        return json.dumps(data, indent=indent)


# 加密工具类
class EncryUtils:
    @staticmethod
    def get_md5_by_url(url, end_with_symbol=False, symbol='/'):
        """
        获取url的md5值
        :param url:
        :param end_with_symbol: url是否需要以斜杠结尾
        :param symbol: url结尾的符号
        :return: md5_value url的md5值
        """
        if end_with_symbol and not url.endswith(symbol):
            url += symbol
        md5_hash = hashlib.md5(url.encode()).hexdigest()
        return md5_hash
